import random
import re
from datetime import datetime

from flask import request, abort, current_app, make_response, jsonify, session

from info import sr, db
from info.lib.yuntongxun.sms import CCP
from info.models import User
from info.modules.passport import passport_blu
from info.utils.captcha.pic_captcha import captcha
from info.utils.response_code import RET, error_map  # type: RET

"""获取图片验证码"""
# 思路
# 1，浏览器要获取图片验证码，信息不敏感，用get方式在URL后面拼接，
#   并将图片验证码对应的key_id传给服务器
# 2，服务器先从URL获取到key_id，判断key_id是否存在，
# 如果存在，生成图片验证码的图片和文字，发给浏览器
# 如果不存在，抛异常
# 然后将key和图片验证码对应的文字保存到redis数据库
"""获取短信验证码"""
# 思路
# 1，浏览器将用户输入的手机号和图片验证码文字和key_id发给服务器
#   请求获取短信验证码
# 2，服务器将三者获取出来，先校验三者是否都存在，如果不存在抛异常
#   如果都有，那么根据用户输入的key_id查询真实的验证码文字，
#   再与获取到的用户输入的图片验证码文字进行比对
# 3， 如果相同，将短信验证码用第三方短信验证码发给对应的手机号，
#    并将短信验证码保存到redis数据库
"""用户注册"""
# 思路
# 1，浏览器将用户手机号和密码还有用户输入的短信验证码发给服务器
# 2，服务器先获取参数，在redis数据库取出短信验证码，然后和用户输入的
#   进行比较，如果短信验证码相同，那么将手机号和验证码存进MySQL数据库
# 使用passport_blu装饰路由

"""获取图片验证码"""


@passport_blu.route('/get_img_code')
def get_img_code():
    # 思路
    # 1，浏览器要获取图片验证码，信息不敏感，用get方式在URL后面拼接，
    #   并将图片验证码对应的key_id传给服务器
    # 2，服务器先从URL获取到key_id，判断key_id是否存在，
    # 如果存在，生成图片验证码的图片和文字，发给浏览器
    # 如果不存在，抛异常
    # 然后将key和图片验证码对应的文字保存到redis数据库

    # 1，获取参数，key_id,用args获取
    img_code_id = request.args.get("img_code_id")
    # 2,校验参数，img_code_id是否存在
    if not img_code_id:
        # 如果不存在，抛异常
        return abort(403)
    # 3,存在就用第三方生成图片验证码图片和文字
    name, img_code_text, img_code_bytes = captcha.generate_captcha()
    # 4,将图片的key_id和图片验证码文字存进redis数据库,需要进行异常捕捉
    try:
        # 验证码设置过期时间，为5分钟300秒
        sr.set("img_code_id" + img_code_id, img_code_text, ex=300)
    except BaseException as e:
        current_app.logger.error(e)
        return abort(500)
    # 打印后的结果是二进制的，所以在redis数据库中，应该去设置自动解码
    print(sr.get('img_code_id' + img_code_id))
    # 5，创建自定义响应对象，将图片验证码放进响应对象，并将类型改成图片类型
    response = make_response(img_code_bytes)  # type: Response
    response.content_type = "image/jpeg"
    # 6，返回响应对象给浏览器
    return response


"""获取短信验证码"""


@passport_blu.route('/get_sms_code', methods=['POST'])
def get_sms_code():
    # 思路
    # 1，浏览器将用户输入的手机号和图片验证码文字和key_id发给服务器
    #   请求获取短信验证码
    # 2，服务器将三者获取出来，先校验三者是否都存在，如果不存在抛异常
    #   如果都有，那么根据用户输入的key_id查询真实的验证码文字，
    #   再与获取到的用户输入的图片验证码文字进行比对
    # 3， 如果相同，将短信验证码用第三方短信验证码发给对应的手机号，
    #    并将短信验证码保存到redis数据库
    # 1，获取参数，从请求中获取图片的key_id(img_code_id)，和手机号和用户输入的图片验证码文字
    #   因为前端使用json发送，所以获取的时候直接用json获取即可
    img_code_id = request.json.get("img_code_id")
    mobile = request.json.get("mobile")
    img_code = request.json.get("img_code")
    # 2，校验参数，校验三者是否都有，如果有一个获取不到，那么json返回一个参数错误的异常
    if not all([img_code_id, mobile, img_code]):
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
    # 3，如果都能获取到，那么用正则匹配手机号是否符合，如果不符，那么json返回一个参数错误的异常
    if not re.match(r"1[35678]\d{9}$", mobile):
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
    #3.1,判断手机号是否已经注册过，即数据库中能不能取出手机号
    #根据手机号从数据库取出数据(记得进行异常捕捉)
    try:
        user = User.query.filter_by(mobile=mobile).first()
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])
    #如果用户已经注册，json返回用户已存在
    if user:
        return jsonify(errno=RET.DATAEXIST, errmsg="用户已存在")
    # 4，如果以上都符合，那么从redis数据库中根据参数中的key_id取出图片验证码的文字（从数据库取数据一定需要进行异常捕捉，防止取出数据失败），
    try:
        real_img_text = sr.get("img_code_id" + img_code_id)
    except BaseException as e:
        # 将数据库异常输出到控制台
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])
    # 5，判断图片验证码是否过期，此时的验证码是可以取出，值为None，如果过期那么返回参数错误，并将错误信息直接定为“验证码已过期”
    if not real_img_text:
        return jsonify(errno=RET.PARAMERR, errmsg="验证码已过期")
    # 6，校验图片验证码文字是否正确，将用户输入的图片验证码文字和redis数据库中取出的对比
    # 7,如果不相等，那么用json返回一个参数错误，并将错误直接定为验证码错误
    #  为了用户体验更好，可以不区分大小写，第三方生成的验证码都是大写的
    if real_img_text != img_code.upper():
        # 验证码错误
        return jsonify(errno=RET.PARAMERR, errmsg="验证码错误")
    # 8，如果相等，验证码正确，那么发短信验证码，用第三方插件发送一个随机数给参数中的用户手机号
    #   生成随机数
    rand_num = "%04d" % random.randint(0, 9999)
    # 输出短信验证码到控制台
    info = current_app.logger.info("短信验证码为：%s" % rand_num)
    # 发给用户手机号,mobile是手机号，【rand_num,1】是短信验证码，和1分钟内失效，后面的1是模板的代号，一般都用1
    # result = CCP().send_template_sms(mobile, [rand_num, 1], 1)
    # if result == -1:
    #     #如果发送失败，用json返回第三方系统错误
    #     return jsonify(errno=RET.THIRDERR,errmsg=error_map[RET.THIRDERR])
    # 9，将短信验证码保存到redis数据库中(从数据库存数据一定需要进行异常捕捉，防止存储数据失败)
    try:
        sr.set("sms_code_" + mobile, rand_num, ex=60)
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR,errmsg=error_map[RET.DBERR])
    # 10，将发送成功的结果用json返回给前端
    return jsonify(errno=RET.OK,errmsg=error_map[RET.OK])


"""用户注册"""
@passport_blu.route('/register',methods=['POST'])
def register():
    # 思路
    # 1，浏览器将用户手机号和密码还有用户输入的短信验证码发给服务器
    # 2，服务器先获取参数，在redis数据库取出短信验证码，然后和用户输入的
    #   进行比较，如果短信验证码相同，那么将手机号和验证码存进MySQL数据库

    #1,获取参数，用户手机号、密码、用户输入的短信验证码
    mobile = request.json.get("mobile")
    password = request.json.get("password")
    sms_code = request.json.get("sms_code")
    #2校验参数，是否都不为空，
    if not all([mobile,password,sms_code]):
        return jsonify(errno=RET.PARAMERR,errmsg=error_map[RET.PARAMERR])
    #校验手机号码是否符合正则
    if not re.match(r"1[35678]\d{9}$", mobile):
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])

    #4从redis数据库中取出短信验证码（记得进行异常捕捉）
    try:
        real_sms_code = sr.get("sms_code_" + mobile)
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR,errmsg=error_map[RET.DBERR])
    #3校验短信验证码是否过期
    if not real_sms_code:
        return jsonify(errno=RET.PARAMERR,errmsg="短信验证码过期")
    #5校验用户输入的短信验证码是否正确
    if real_sms_code != sms_code:
        return jsonify(errno=RET.PARAMERR,errmsg="短信验证码错误")
    #6如果正确，那么将数据库保存到MySQL数据库中，进行异常捕捉和回滚
    #创建User表模型对象
    user = User()
    #给User表模型加入数据,即给属性赋值
    user.mobile = mobile
    # user.password_hash = mobile
    #密码在表里要进行封装加密，有一个被property装饰的password方法
    #也相当于把password放进表里了
    user.password = password
    user.nick_name = mobile   #昵称
    #记录用户的最后登录时间，创建表的类时也要考虑到这个字段，后来的后台数据分析要用到
    # 改变了MySQL数据库，需要提交，可是每次都需要提交，可以使用flask的自动提交，去config.py里面设置
    user.last_login = datetime.now()

    try:
        db.session.add(user)
        db.session.commit()
    except BaseException as e:
        current_app.logger.error(e)
        #回滚
        db.session.rollback()
        #给前端json返回异常
        return jsonify(errno=RET.DBERR,errmsg=error_map[RET.DBERR])
    #状态保持用session,保存模型对象的主键（为后面的免密码登录用）
    session["user_id"] = user.id
    #7给前端json返回验证结果
    return jsonify(errno=RET.OK,errmsg=error_map[RET.OK])


"""用户登录"""
@passport_blu.route('/login',methods=['POST'])
def login():
    #思路
    #获取参数，手机号和密码
    #校验参数是否完整，如果不完整json返回异常
    #校验手机号是否符合正则
    #根据手机号从数据库取出模型对象，记得进行异常捕捉
    #判断取出的对象是否为空，为空就代表没有注册过，json返回异常、
    #判断密码是否正确，不正json返回异常
    #如果都正确返回登录成功的结果给前端

    # 获取参数，手机号和密码
    mobile = request.json.get("mobile")
    password = request.json.get("password")
    # 校验参数是否完整，如果不完整json返回异常
    if not all([mobile,password]):
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
    # 校验手机号是否符合正则
    if not re.match(r"1[35678]\d{9}$",mobile):
        return jsonify(errno=RET.PARAMERR, errmsg=error_map[RET.PARAMERR])
    # 根据手机号从数据库取出模型对象，记得进行异常捕捉
    try:
        user = User.query.filter_by(mobile=mobile).first()
    except BaseException as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg=error_map[RET.DBERR])
    # 判断取出的对象是否为空，为空就代表没有注册过，json返回异常、
    if not user:
        return jsonify(errno=RET.USERERR, errmsg=error_map[RET.USERERR])
    # 判断密码是否正确，不正确json返回异常，因为密码是用fla加密过的，所以要用flask相应的解码并核对功能
    if not user.check_password(password):
        return jsonify(errno=RET.PARAMERR, errmsg="密码错误")
    #记录最后的登录时间，后面数据分析可以使用
    user.last_login = datetime.now()
    #状态保持以便后续免密码登录
    session["user_id"] = user.id
    # 如果都正确返回登录成功的结果给前端
    return jsonify(errno=RET.OK, errmsg=error_map[RET.OK])


"""用户登出"""
@passport_blu.route('/logout')
def logout():
    #前端有实现点击退出按钮就用跟路由重新刷新界面，所以最后还是要走跟路由
    #而跟路由是根据session能不能取出值来判断是否登录，所以登出只需要将session中的user_id删除即可
    session.pop("user_id",None)
    session.pop("is_admin",None)
    return jsonify(errno=RET.OK, errmsg=error_map[RET.OK])


